﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Security.Cryptography;
using System.Threading;
using System.IO;
using System.Diagnostics;

namespace AsteriskAPI
{
    public class AsteriskConnection
    {
        #region Constants
        const int COMMAND_RESPONSE_TIMEOUT = 1000;
        #endregion

        #region Events
        public event EventHandler<AsteriskEventArgs> EventOccurred;
        protected virtual void OnEventOccurred(AsteriskEventArgs e)
        {
            if (EventOccurred != null) EventOccurred(this, e);
        }
        #endregion

        #region Local Variables and Properties
        private string host;
        public string Host
        {
            get { return host; }
        }

        private int port;
        public int Port
        {
            get { return port; }
        }

        private string serverID;
        public string ServerID
        {
            get { return serverID; }
        }
        
        private Socket serverSocket;
        private NetworkStream serverNetworkStream;
        private StreamWriter serverWriter;
        private StreamReader serverReader;

        protected object commandLock = new object();
        protected Dictionary<string, AsteriskResponse> commandResponses = new Dictionary<string, AsteriskResponse>();

        #endregion

        #region Initialization and Release
        public AsteriskConnection(string host, int port) : this(host, port, String.Empty) { }
        public AsteriskConnection(string host, int port, string serverID)
        {
            this.host = host;
            this.port = port;
            this.serverID = serverID;
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        ~AsteriskConnection()
        {
            CloseConnection();
        }

        public void Connect()
        {
            serverSocket.Connect(host, port);
            serverNetworkStream = new NetworkStream(serverSocket);
            serverWriter = new StreamWriter(serverNetworkStream);
            serverReader = new StreamReader(serverNetworkStream);
        }

        public void Login(string user, string secret)
        {
            /*
            ReceiveData();
            Send(String.Format("Action: Login\r\nActionID: 1\r\nUsername: {0}\r\nSecret: {1}\r\n\r\n", user, secret));
            string response;
            do {
                response = ReceiveData();
            } while (!response.Contains("Response"));
            if (!response.Contains("Success"))
                throw new Exception("Login Failed:\n\n" + response);
            */
            ExecuteCommand("Login", new Dictionary<string, string>() { { "Username", user }, { "Secret", secret } });
        }

        public void CloseConnection()
        {
            serverSocket.Disconnect(false);
        }

        #endregion

        #region Outbound communication methods
        public void Send(string message)
        {
            lock (commandLock)
            {
                Debug.WriteLine("commandLock locked by Send()");
                //serverSocket.Send(Encoding.ASCII.GetBytes(message));
                serverWriter.Write(message);
                serverWriter.Flush();
                Debug.WriteLine("commandLock unlocked by Send()");
            }
        }

        #endregion

        #region Inbound communications methods
        protected string ReceiveData()
        {
            /*
            byte[] buffer = new byte[1024];
            serverSocket.Receive(buffer);
            return Encoding.UTF8.GetString(buffer);
            */
            char[] buffer = new char[1024];
            for (int i = 0; i < 1024; i++)
            {
                serverReader.Read(buffer,i,1);
                //Debug.WriteLineIf(buffer.Length > 0, "\n\nbuffer: \n" + new string(buffer));
                if (i >= 3 && buffer[i-3] == '\r' && buffer[i-2] == '\n' && buffer[i-1] == '\r' && buffer[i] == '\n')
                    break;
            }
            string retVal = new string(buffer);
            return retVal;
        }

        protected Dictionary<string, string> GetDataAsDict()
        {
            string response = ReceiveData();
            string[] separators = { ": " };
            Dictionary<string, string> dict = new Dictionary<string, string>();
            foreach (string str in response.Split('\n'))
            {
                if (!str.Contains(": "))
                    continue;
                string[] kvPair = str.Split(separators, 2, StringSplitOptions.None);
                if (kvPair.Length != 2)
                    continue;
                dict[kvPair[0].Trim()] = kvPair[1].Trim();
            }
            /*
            Debug.WriteLineIf(dict.Keys.Contains("Event"), "\nGetDataAsDict() returning value: \n");
            Debug.Indent();
            foreach (var entry in dict)
                Debug.WriteLineIf(dict.Keys.Contains("Event"), String.Format("{0} => {1}", entry.Key, entry.Value));
            Debug.Unindent();*/
            return dict;
        }

        public void WatchAsteriskLog()
        {
            Action action = delegate()
            {
                string actionID;
                EventWaitHandle waitHandle = null;
                bool setWaitHandle;
                while (true)
                {
                    Dictionary<string, string> info = GetDataAsDict();
                    if (info.ContainsKey("Event")) // if this is an event, handle it
                    {
                        OnEventOccurred(new AsteriskEventArgs(info["Event"], info));
                    }
                    if (info.ContainsKey("Response") && info.ContainsKey("ActionID"))
                    {
                        actionID = info["ActionID"];
                        setWaitHandle = false;
                        waitHandle = null;
                        lock (commandResponses)
                        {
                            Debug.WriteLine("commandResponses locked by WatchAsteriskLog()");
                            if (commandResponses.ContainsKey(actionID))
                            {
                                Debug.WriteLine("value of info:");
                                Debug.Indent();
                                foreach (var entry in info)
                                    Debug.WriteLine(String.Format("{0} => {1}", entry.Key, entry.Value));
                                Debug.Unindent();
                                Debug.WriteLine("Existing keys in commandResponses");
                                Debug.Indent();
                                foreach (var item in commandResponses)
                                {
                                    Debug.WriteLine(item.Key);
                                    Debug.Indent();
                                    Debug.WriteLine("ActionID: " + item.Value.ActionID);
                                    Debug.WriteLine("CreatedDateTime: " + item.Value.CreatedDateTime.ToString());
                                    Debug.WriteLine("WaitHandle: " + item.Value.WaitHandle.ToString());
                                    Debug.WriteLine("WaitHandle is null? " + (item.Value.WaitHandle == null).ToString());
                                    Debug.Unindent();
                                }
                                Debug.Unindent();
                                commandResponses[actionID].Success = (info["Response"] == "Success");
                                commandResponses[actionID].Message = info["Message"];
                                setWaitHandle = commandResponses[actionID].ReturnControlOnResponse;
                                waitHandle = commandResponses[actionID].WaitHandle;
                            }
                            Debug.WriteLine("commandResponses unlocked by WatchAsteriskLog()");
                        }
                        if (setWaitHandle && waitHandle != null)
                        {
                            waitHandle.Set(); 
                            Debug.WriteLine("releasing waitHandle for actionID #" + actionID);
                        }
                    }
                }
            };
            ThreadStart threadStart = new ThreadStart(action);
            new Thread(threadStart).Start();
        }

        #endregion

        #region Command Interface

        //protected const int MAX_COMMAND_WAIT_TIME = 10;

        public string GenerateActionID()
        {
            return this.serverID + System.Guid.NewGuid().ToString();
        }

        public void ExecuteCommand(string name, Dictionary<string, string> args)
        {
            string actionID = GenerateActionID();
            ExecuteCommand(name, args, actionID);
        }
        public void ExecuteCommand(string name, Dictionary<string,string> args, string actionID)
        {
            Action action = delegate()
            {
                lock (commandResponses)
                {
                    Debug.WriteLine("commandResponses locked by ExecuteCommand(" + actionID + ") child thread");
                    AsteriskResponse response = commandResponses[actionID];
                    commandResponses.Remove(actionID);
                    Debug.WriteLine("commandResponses unlocked by ExecuteCommand(" + actionID + ") child thread");
                }
            };
            ExecuteCommand(name, args, actionID, action, false);
        }
        public void ExecuteCommand(string name, Dictionary<string, string> args, string actionID, Action action, bool joinThread)
        {
            ExecuteCommand(name, args, actionID, action, joinThread, COMMAND_RESPONSE_TIMEOUT);
        }
        public void ExecuteCommand(string name, Dictionary<string, string> args, string actionID, Action action, bool joinThread, int joinTimeout)
        {
            string commandString = "Action: " + name + "\r\n";
            args["ActionID"] = actionID;
            foreach (var arg in args)
                commandString += arg.Key + ": " + arg.Value + "\r\n";
            commandString += "\r\n";

            EventWaitHandle waitHandle = new AutoResetEvent(false);
            Action newAction = delegate() {
                Send(commandString);
                waitHandle.WaitOne();
                action(); 
            };
            ThreadStart threadStart = new ThreadStart(newAction);

            lock (commandResponses)
            {
                Debug.WriteLine("commandResponses locked by ExecuteCommand(" + actionID + ")");
                commandResponses[actionID] = new AsteriskResponse(waitHandle, actionID);
                Debug.WriteLine("commandResponses unlocked by ExecuteCommand(" + actionID + ")");
            }

            Thread thread = new Thread(threadStart);
            thread.Start();
            if (joinThread)
                thread.Join(joinTimeout);
        }

        public AsteriskResponse ExecuteCommandAndGetResponse(string name, Dictionary<string, string> args)
        {
            AsteriskResponse retVal = null;
            string actionID = GenerateActionID();
            Action action = delegate()
            {
                lock (commandResponses)
                {
                    Debug.WriteLine("commandResponses locked by ExecuteCommand(" + actionID + ") child thread");
                    retVal = commandResponses[actionID];
                    commandResponses.Remove(actionID);
                    Debug.WriteLine("commandResponses unlocked by ExecuteCommand(" + actionID + ") child thread");
                }
            };
            ExecuteCommand(name, args, actionID, action, true);
            return retVal;
        }

        public void ExecuteCommandAndPerformAction(string name, Dictionary<string, string> args, string eventName, Action<AsteriskEventArgs> action)
        {
            string actionID = GenerateActionID();
            EventHandler<AsteriskEventArgs> addToListEventHandler = null;
            Action<object, AsteriskEventArgs> addToListDelegate = delegate(object sender, AsteriskEventArgs e)
            {
                if (e.Info.ContainsKey("ActionID") && e.Info["ActionID"] == actionID && e.Name == eventName)
                {
                    Debug.WriteLine("EventOccurred length before removal: " + EventOccurred.GetInvocationList().Length);
                    action(e);
                    if (addToListEventHandler != null)
                        this.EventOccurred -= addToListEventHandler;
                    Debug.WriteLine("EventOccurred length after removal: " + EventOccurred.GetInvocationList().Length);
                }
            };
            addToListEventHandler = new EventHandler<AsteriskEventArgs>(addToListDelegate);
            this.EventOccurred += addToListEventHandler;

            ExecuteCommand(name, args, actionID);
        }

        public List<AsteriskEventArgs> ExecuteCommandAndReturnList(string name, string endListEventName)
        {
            return ExecuteCommandAndReturnList(name, new Dictionary<string, string>(), endListEventName);
        }

        public List<AsteriskEventArgs> ExecuteCommandAndReturnList(string name, Dictionary<string, string> args, string endListEventName)
        {
            string actionID = GenerateActionID();
            List<AsteriskEventArgs> retVal = new List<AsteriskEventArgs>();
            EventWaitHandle waitHandle = new AutoResetEvent(false);


            string commandString = "Action: " + name + "\r\n";
            args["ActionID"] = actionID;
            foreach (var arg in args)
                commandString += arg.Key + ": " + arg.Value + "\r\n";
            commandString += "\r\n";

            Action<object, AsteriskEventArgs> addToListDelegate = delegate(object sender, AsteriskEventArgs e)
            {
                if (e.Info.ContainsKey("ActionID") && e.Info["ActionID"] == actionID)
                {
                    lock (retVal)
                    {
                        Debug.WriteLine("retVal locked by ExecuteCommandAndReturnList() child thread");
                        retVal.Add(e);
                        Debug.WriteLine("retVal unlocked by ExecuteCommandAndReturnList() child thread");
                    }
                    if (e.Name == endListEventName)
                    {
                        waitHandle.Set();
                    }
                }
            };
            EventHandler<AsteriskEventArgs> addToListEventHandler = new EventHandler<AsteriskEventArgs>(addToListDelegate);
            this.EventOccurred += addToListEventHandler;

            Action action = delegate()
            {
                Send(commandString);
                waitHandle.WaitOne();
                lock (commandResponses)
                {
                    Debug.WriteLine("commandResponses locked by ExecuteCommandAndReturnList() child thread");
                    AsteriskResponse response = commandResponses[actionID];
                    commandResponses.Remove(actionID);
                    Debug.WriteLine("commandResponses unlocked by ExecuteCommandAndReturnList() child thread");
                }
            };


            lock (commandResponses)
            {
                Debug.WriteLine("commandResponses locked by ExecuteCommandAndReturnList()");
                commandResponses[actionID] = new AsteriskResponse(waitHandle, actionID, false);
                Debug.WriteLine("commandResponses unlocked by ExecuteCommandAndReturnList()");
            }

            ThreadStart threadStart = new ThreadStart(action);            
            Thread thread = new Thread(threadStart);
            thread.Start();
            thread.Join();

            this.EventOccurred -= addToListEventHandler;
            if (retVal.Where(o => o.Name == endListEventName).Count() == 0)
                throw new Exception("The event appears to have timed out.");
            return retVal;
        }

        #region Specific Commands

        #endregion

        #endregion
    }
}
